'---------------------------------------------------------------------------------------------------
'STRING MANIPULATION
'---------------------------------------------------------------------------------------------------
'SUMMARY:
'   Purpose:  A library of custom functions that transform strings.
'   Author:   Dustinian Camburides (dustinian@gmail.com)
'   Platform: QB64 (www.qb64.net)
'   Revision: 1.7
'   Updated:  4/8/2013
'---------------------------------------------------------------------------------------------------
'REVISION HISTORY
'	  1.7: Fixed: Logic error in [Replace_Subsequent$].
'   1.6: Amend: Used pointers (rather than INSTR functions) in WHILE statements (increased efficiency).
'               Removed procedure-level [strText] variable (reduced memory footprint).
'               Removed "IF" statements from pointer calculations (increased efficiency).
'   1.5: Fixed: Made all pointers (Location, Start, Length, etc.) LONG instead of INTEGER for strings greater than 32kb.
'   1.4: Amend: [Replace$] renamed to [Replace_Once$], and [Replace_All$] renamed to [Replace$] for clarity.
'   1.3: Fixed: Infinite loop in [Replace_From$] when [Add] sub-string was zero-length.
'               Infinite loop in [Replace_Between$] when [Add] sub-string was zero-length.
'               Infinite loop in [Replace_Subsequent$] when [Add] sub-string was zero-length.
'               Logic error in [Replace_If_Between$] where only the first [Find] was replaced.
'   1.2: Added: Detailed comments on the purpose, inputs, and outputs added to each function.
'   1.1: Added: [Start] parameter for the [Between$] function.
'               [Start] parameter for the [Before$] function.
'               [Start] parameter for the [After$] function.
'               [Start] parameter for the [Replace_Once$] function.
'        Fixed: Runtime error in [Replace_From$] function when [Precedant] or [Antecedant] did not appear.
'               Runtime error in [Replace_Between$] function when [Precedant] or [Antecedant] did not appear.
'               Runtime error in [Replace_Before_Next$] function when [Precedant] or [Antecedant] did not appear.
'               Runtime error in [Replace_Subsequent$] function when [Precedant] or [Antecedant] did not appear.
'               Runtime error in [Replace_If_Between$] function when [Precedant] or [Antecedant] did not appear.
'               Runtime error in [Between$] function when [Precedant] or [Antecedant] did not appear.
'               Invalid output from [Before$] function when [Find] did not appear.
'               Invalid output from [After$] function when [Find] did not appear.
'               Invalid output from [Between$] function when [Find] did not appear.
'               Invalid output from [Replace_Once$] function when [Find] did not appear.
'   1.0: First working version.
'---------------------------------------------------------------------------------------------------
'PLANNED ENHNACEMENTS
'   Minor:
'       - Add scope control (ByVal, ByRef) to each parameter in each function.
'       - Add scope control (Public, Private) to each function.
'   Major:
'       - Make the [Start] parameter optional for [Replace_Once$] function.
'       - Make the [Start] parameter optional for [Between$] function.
'       - Make the [Start] parameter optional for [Before$] function.
'       - Make the [Start] parameter optional for [After$] function.
'---------------------------------------------------------------------------------------------------
'PROCEDURES
'   - FUNCTION Replace_From$ (Text AS STRING, Precedant AS STRING, Antecedant AS STRING, Add AS STRING)
'   - FUNCTION Replace_Between$ (Text AS STRING, Precedant AS STRING, Antecedant AS STRING, Add AS STRING)
'   - FUNCTION Replace_Subsequent$ (Text AS STRING, Precedant AS STRING, Find AS STRING, Add AS STRING)
'   - FUNCTION Replace_If_Between$ (Text AS STRING, Precedant AS STRING, Antecedant AS STRING, Find AS STRING, Add AS STRING)
'   - FUNCTION Replace$ (Text AS STRING, Find AS STRING, Add AS STRING)
'   - FUNCTION Between$ (Text AS STRING, Precedant AS STRING, Antecedant AS STRING, Start as INTEGER)
'   - FUNCTION Before$ (Text AS STRING, Find AS STRING, Start AS INTEGER)
'   - FUNCTION After$ (Text AS STRING, Find AS STRING, Start AS INTEGER)
'   - FUNCTION Filter$ (Text AS STRING, Allowed AS STRING)
'---------------------------------------------------------------------------------------------------
FUNCTION Replace_From$ (Text AS STRING, Precedant AS STRING, Antecedant AS STRING, Add AS STRING)'
    'SUMMARY:
       '[Replace_From$] replaces characters in the [Text] string from the start of the [Precedant] sub-string to the end of the [Antecedant] sub-string with the [Add] sub-string.
    'INPUT:
        'Text: The input string; the text that's being manipulated.
        'Precedant: The sub-string that denotes the start of the change within [Text].
        'Antecedant: The sub-string that denotes the end of the change within [Text].
        'Add: The sub-string that's being added to the [Text] string.
    'VARIABLES:
        DIM lngLocation AS LONG     'The address of the [Find] substring within the [Text] string.
        DIM lngStart AS LONG        'The location of the [Precedant] sub-string within the [Text] string.
        DIM lngStop AS LONG         'The location of the [Antecedant] sub-string within the [Text] string.
    'INITIALIZE:
        'Locate the [Precedant]:
            lngStart = INSTR(1, Text, Precedant)
    'PROCESSING:
        'While the [Precedant] appears in the [Text]...
            WHILE lngStart
                'Locate the [Antecedant]:
                    lngStop = INSTR((lngStart + LEN(Precedant)), Text, Antecedant)
                'If the [Antecedant] appears in the [Text]...
                    IF lngStop THEN
                        'Locate the last character of the [Antecedant]:
                            lngStop = lngStop + LEN(Antecedant) - 1
                        'Replace the text:
                            Text = LEFT$(Text, (lngStart - 1)) + Add + RIGHT$(Text, (LEN(Text) - lngStop))
                    ELSE
                        'Increment the start point:
                            lngStart = lngStart + INSTR((lngStart + 1), Text, Precedant)
                    END IF
                'Increment the start point:
                    lngStart = INSTR((lngStart + LEN(Precedant) + LEN(Add) + LEN(Antecedant)), Text, Precedant)
        'Next instance of [Precedant]...
            WEND
    'OUTPUT:
        Replace_From$ = Text
END FUNCTION

FUNCTION Replace_Between$ (Text AS STRING, Precedant AS STRING, Antecedant AS STRING, Add AS STRING)
    'SUMMARY:
       '[Replace_From$] replaces characters in the [Text] string from the end of the [Precedant] sub-string to the start of the [Antecedant] sub-string with the [Add] sub-string.
    'INPUT:
        'Text: The input string; the text that's being manipulated.
        'Precedant: The sub-string that denotes the start of the change within [Text].
        'Antecedant: The sub-string that denotes the end of the change within [Text].
        'Add: The sub-string that's being added to the [Text] string.
    'VARIABLES:
        DIM lngLocation AS LONG     'The address of the [Find] substring within the [Text] string.
        DIM lngStart AS LONG        'The location of the [Precedant] sub-string within the [Text] string.
        DIM lngStop AS LONG         'The location of the [Antecedant] sub-string within the [Text] string.
    'INITIALIZE:
        'Locate the [Precedant]:
            lngStart = INSTR(1, Text, Precedant)
    'PROCESSING:
        'While the [Precedant] appears in the [Text]...
            WHILE lngStart
                'Locate the [Antecedant] in the [Text]...
                    lngStop = INSTR((lngStart + LEN(Precedant)), Text, Antecedant)
                'If the [Antecedant] appears in the [Text]...
                    IF lngStop THEN
                        'Locate the last character of the [Antecedant]:
                            lngStop = lngStop - 1
                        'Replace the text:
                            Text = LEFT$(Text, (lngStart + LEN(Precedant) - 1)) + Add + RIGHT$(Text, (LEN(Text) - lngStop))
                    ELSE
                        'Increment the start point:
                            lngStart = INSTR((lngStart + 1), Text, Precedant)
                    END IF
            'Increment the start point:
                lngStart = INSTR((lngStart + LEN(Precedant) + LEN(Add) + LEN(Antecedant)), Text, Precedant)
        'Next instance of [Precedant]...
            WEND
    'OUTPUT:
        Replace_Between$ = Text
END FUNCTION

FUNCTION Replace_Subsequent$ (Text AS STRING, Precedant AS STRING, Find AS STRING, Add AS STRING)
    'SUMMARY:
       '[Replace_Subsequent$] replaces the first [Find] sub-string after the [Precedant] sub-string with the [Add] sub-string within the [Text] string.
    'INPUT:
        'Text: The input string; the text that's being manipulated.
        'Precedant: The sub-string that denotes the start of the change within [Text].
        'Find: The specified sub-string; the string sought within the [Text] string.
        'Add: The sub-string that's being added to the [Text] string.
    'VARIABLES:
        DIM lngStart AS LONG        'The location of the [Precedant] sub-string within the [Text] string.
        DIM lngStop AS LONG         'The location of the last character of the [Find] sub-string within the [Text] string.
        DIM lngLocation AS LONG     'The address of the [Find] substring within the [Text] string.
    'INITIALIZE:
        lngOldStart = 0
        lngStart = INSTR(1, Text, Precedant)
    'PROCESSING:
        'While the [Precedant] appears in the [Text]...
            WHILE lngStart
                'Locate the [Find] sub-string:
                    lngLocation = INSTR((lngStart + Len(Precedant) - 1), Text, Find)
                'If [Find] appears in the [Text]...
                    IF lngLocation THEN
                        'Locate the last character of [Find]:
                            lngStop = lngLocation + LEN(Find) - 1
                        'Replace the text:
                            Text = LEFT$(Text, (lngLocation - 1)) + Add + RIGHT$(Text, (LEN(Text) - lngStop))
                        'Increment the start point:
                            lngStart = INSTR((lngStart + LEN(Precedant) + LEN(Add)), Text, Precedant)
                    ELSE
                        'Increment the start point:
                            lngStart = INSTR((lngStart + 1), Text, Precedant)
                    END IF
        'Next instance of [Precedant]...
            WEND
    'OUTPUT:
        Replace_Subsequent$ = Text
END FUNCTION

FUNCTION Replace_If_Between$ (Text AS STRING, Precedant AS STRING, Antecedant AS STRING, Find AS STRING, Add AS STRING)
    'SUMMARY:
       '[Replace_If_Between$] Replaces all instances of the [Find] sub-string that appear between the [Precedant] and [Antecedant] sub-strings with the [Add] sub-string in the [Text] string.
    'INPUT:
        'Text: The input string; the text that's being manipulated.
        'Precedant: The sub-string that denotes the start of the change within [Text].
        'Antecedant: The sub-string that denotes the end of the change within [Text].
        'Find: The specified sub-string; the string sought within the [Text] string.
        'Add: The sub-string that's being added to the [Text] string.
    'VARIABLES:
        DIM lngStart AS LONG        'The location of the [Precedant] sub-string within the [Text] string.
        DIM lngLocation AS LONG     'The address of the [Find] substring within the [Text] string.
        DIM strSubString AS STRING  'The text between the [Precedant] and [Antecedant] sub-strings within the [Text] string.
    'INITIALIZE:
        'Locate the [Precedant] string within the [Text] string:
            lngStart = INSTR(1, Text, Precedant)
    'PROCESSING:
        'While the [Precedant] appears in the [Text]...
            WHILE lngStart
                'Locate the [Antecedant]:
                    lngLocation = INSTR((lngStart + LEN(Precedant)), Text, Antecedant)
                'If the [Antecedant] appears in the [Text]...
                    IF lngLocation THEN
                        'Extract the substring:
                            strSubString = MID$(Text, (lngStart + LEN(Precedant)), (lngLocation - (lngStart + LEN(Precedant))))
                        'Replace the text within the substring:
                            strSubString = Replace$(strSubString, Find, Add)
                        'Insert the new substring into the text:
                            Text = LEFT$(Text, ((lngStart + LEN(Precedant)) - 1)) + strSubString + RIGHT$(Text, (LEN(Text) + 1 - lngLocation))
                    ELSE
                        'Increment the start point:
                            lngStart = INSTR(lngLocation, Text, Precedant)
                    END IF
                'Increment the start point:
                    lngStart = INSTR(lngStart + LEN(Precedant) + LEN(strSubString) + LEN(Antecedant), Text, Precedant)
        'Next instance of [Precedant]...
            WEND
    'OUTPUT:
        Replace_If_Between$ = Text
END FUNCTION

FUNCTION Replace$ (Text AS STRING, Find AS STRING, Add AS STRING)
    'SUMMARY:
       '[Replace$] replaces all instances of the [Find] sub-string with the [Add] sub-string within the [Text] string.
    'INPUT:
        'Text: The input string; the text that's being manipulated.
        'Find: The specified sub-string; the string sought within the [Text] string.
        'Add: The sub-string that's being added to the [Text] string.
    'VARIABLES:
        DIM lngLocation AS LONG 'The address of the [Find] substring within the [Text] string.
        DIM strBefore AS STRING 'The characters before the string to be replaced.
        DIM strAfter AS STRING  'The characters after the string to be replaced.
    'INITIALIZE:
            lngLocation = INSTR(1, Text, Find)
    'PROCESSING:
        'While [Find] appears in [Text]...
            WHILE lngLocation
                'Extract all text before the [Find] substring:
                    strBefore = LEFT$(Text, lngLocation - 1)
                'Extract all text after the [Find] substring:
                    strAfter = RIGHT$(Text, ((LEN(Text) - (lngLocation + LEN(Find) - 1))))
                'Return the substring:
                    Text = strBefore + Add + strAfter
                'Locate the next instance of [Find]:
                    lngLocation = INSTR(1, Text, Find)
        'Next instance of [Find]...
            WEND
    'OUTPUT:
        Replace$ = Text
END FUNCTION

FUNCTION Between$ (Text AS STRING, Precedant AS STRING, Antecedant AS STRING, Start AS LONG)
    'SUMMARY:
       '[Between$] returns all the characters from the [Text] string that appear between the [Precedant] and [Antecedant] sub-strings.
    'INPUT:
        'Text: The input string; the text that's being manipulated.
        'Precedant: The sub-string that denotes the start of the change within [Text].
        'Antecedant: The sub-string that denotes the end of the change within [Text].
        'Start: The first location within the [Text] string where the [Find] string is sought.
    'VARIABLES:
        DIM lngLocation AS LONG  'The address of the [Find] substring within the [Text] string.
        DIM lngStart AS LONG     'The address of the end of the [Precedant].
        DIM lngStop AS LONG      'The address of the beginning of the [Antecedant].
        DIM lngLength AS LONG    'The length of the sub-string to be extracted.
    'PROCESSING:
        'Locate the [Precedant]:
            lngLocation = INSTR(Start, Text, Precedant)
        'If the [Precedant] string was present...
            IF lngLocation THEN
                'Move the pointer ot the end of the [Precedant]:
                    lngStart = lngLocation + LEN(Precedant)
                'Locate the [Antecedant]:
                    lngLocation = INSTR(lngStart + 1, Text, Antecedant)
                'If the [Antecedant] was present...
                    IF lngLocation THEN
                        'Calculate the characters to keep:
                            lngLength = (lngLocation - lngStart)
                        'Return the substring:
                            Between$ = MID$(Text, lngStart, lngLength)
                    ELSE
                        'Return a null string:
                            Between$ = ""
                    END IF
            ELSE
                'Return a null string:
                    Between$ = ""
            END IF
END FUNCTION

FUNCTION Before$ (Text AS STRING, Find AS STRING, Start AS LONG)
    'SUMMARY:
       '[Before$] returns all the characters from the [Text] string that appear before the [Find] sub-string.
    'INPUT:
        'Text: The input string; the text that's being manipulated.
        'Find: The specified sub-string; the string sought within the [Text] string.
        'Start: The first location within the [Text] string where the [Find] string is sought.
    'VARIABLES:
        DIM lngLocation AS LONG  'The address of the [Find] substring within the [Text] string.
        DIM lngLength AS LONG    'The length of the sub-string to be extracted.
    'PROCESSING:
        'Locate the [Find] string within the [Text] string:
            lngLocation = INSTR(Start, Text, Find)
        'If the [Find] string was present...
            IF lngLocation THEN
                'Calculate the characters to keep:
                    lngLength = lngLocation - 1
                'Return the substring:
                    Before$ = LEFT$(Text, lngLength)
            ELSE
                'Return a null string:
                    Before$ = ""
            END IF
END FUNCTION

FUNCTION After$ (Text AS STRING, Find AS STRING, Start AS LONG)
    'SUMMARY:
       '[After$] returns all the characters from the [Text] string that appear after the [Find] sub-string.
    'INPUT:
        'Text: The input string; the text that's being manipulated.
        'Find: The specified sub-string; the string sought within the [Text] string.
        'Start: The first location within the [Text] string where the [Find] string is sought.
    'VARIABLES:
        DIM lngLocation AS LONG     'The address of the [Find] substring within the [Text] string.
        DIM lngLength AS LONG       'The length of the [Output] substring.
    'PROCESSING:
        'Locate the [Find] string within the [Text] string:
            lngLocation = INSTR(Start, Text, Find)
        'If the [Find] string was present...
            IF lngLocation THEN
                'Calculate the characters to keep:
                    lngLength = Len(Text) - (lngLocation + LEN(Find) - 1)
                'Return the substring:
                    After$ = RIGHT$(Text, lngLength)
            ELSE
                'Return a null string:
                    After$ = ""
            END IF
END FUNCTION

FUNCTION Filter$ (Text AS STRING, Allowed AS STRING)
    'SUMMARY:
       '[Filter$] removes all characters from the [Text] string that do not appear in the [Allowed] string.
    'INPUT:
        'Text: The input string; the text that's being manipulated.
        'Allowed: The characters allowed in the output.
    'VARIABLES:
        DIM strTemporary AS STRING      'The [Temporary] string that holds the [Output] string as it is assembled.
        DIM strCharacter AS STRING * 1  'The [Character] substring extracted from the [Text] string.
        DIM lngCharacter AS LONG        'The address of the current [Character] in the [Text] string.
    'INITIALIZE:
        strTemporary = ""
    'PROCESSING:
        'For each [Character]...
            FOR lngCharacter = 1 TO LEN(Text)
                'Extract the character:
                    strCharacter = MID$(Text, lngCharacter, 1)
                'If the character appears in the [Allowed] string...
                    IF INSTR(1, Allowed, strCharacter) THEN
                        'Add [Character] to the [Temporary] string:
                            strTemporary = strTemporary + strCharacter
                    END IF
        'Next [Character]...
            NEXT lngCharacter
    'OUTPUT:
        Filter$ = strTemporary
END FUNCTION
